#!/bin/sh
#
#	Description:	IPsrcaddr - Preferred source address modification
#
#	Author:			John Sutton <john@scl.co.uk>
#	Support:		users@clusterlabs.org
#	License:		GNU General Public License (GPL)
#	Copyright:		SCL Internet
#
#	Based on the IPaddr script.
#
#	This script manages the preferred source address associated with
#	packets which originate on the localhost and are routed through the
#	default route.  By default, i.e. without the use of this script or
#	similar, these packets will carry the IP of the primary i.e. the
#	non-aliased interface.  This can be a nuisance if you need to ensure
#	that such packets carry the same IP irrespective of which host in
#	a redundant cluster they actually originate from.
#
#	It can add a preferred source address, or remove one.
#
#	usage: IPsrcaddr {start|stop|status|monitor|validate-all|meta-data}
#
#	The "start" arg adds a preferred source address.
#
#	Surprisingly, the "stop" arg removes it.	:-)
#
#	NOTES:
#
#	1) There must be one and not more than 1 default route!  Mainly because
#	I can't see why you should have more than one.  And if there is more
#	than one, we would have to box clever to find out which one is to be
#	modified, or we would have to pass its identity as an argument.
#
#	2) The script depends on Alexey Kuznetsov's ip utility from the
#	iproute aka iproute2 package.
#
#	3) No checking is done to see if the passed in IP address can
#	reasonably be associated with the interface on which the default
#	route exists.  So unless you want to deliberately spoof your source IP,
#	check it!  Normally, I would expect that your haresources looks
#	something like:
#
#		nodename ip1 ip2 ... ipN IPsrcaddr::ipX
#
#	where ipX is one of the ip1 to ipN.
#
#	  OCF parameters are as below:
#		OCF_RESKEY_ipaddress

#######################################################################
# Initialization:
: ${OCF_FUNCTIONS_DIR=${OCF_ROOT}/lib/heartbeat}
. ${OCF_FUNCTIONS_DIR}/ocf-shellfuncs

# Defaults
OCF_RESKEY_ipaddress_default=""
OCF_RESKEY_cidr_netmask_default=""
OCF_RESKEY_proto_default=""

: ${OCF_RESKEY_ipaddress=${OCF_RESKEY_ipaddress_default}}
: ${OCF_RESKEY_cidr_netmask=${OCF_RESKEY_cidr_netmask_default}}
: ${OCF_RESKEY_proto=${OCF_RESKEY_proto_default}}

#######################################################################

[ -z "$OCF_RESKEY_proto" ] && PROTO="" || PROTO="proto $OCF_RESKEY_proto"

USAGE="usage: $0 {start|stop|status|monitor|validate-all|meta-data}";

  CMDSHOW="$IP2UTIL route show   to exact 0.0.0.0/0"
CMDCHANGE="$IP2UTIL route change to "

SYSTYPE="`uname -s`"

usage() {
	echo $USAGE >&2
}

meta_data() {
	cat <<END
<?xml version="1.0"?>
<!DOCTYPE resource-agent SYSTEM "ra-api-1.dtd">
<resource-agent name="IPsrcaddr">
<version>1.0</version>

<longdesc lang="en">
Resource script for IPsrcaddr. It manages the preferred source address
modification. 
</longdesc>
<shortdesc lang="en">Manages the preferred source address for outgoing IP packets</shortdesc>

<parameters>
<parameter name="ipaddress" unique="0" required="1">
<longdesc lang="en">
The IP address. 
</longdesc>
<shortdesc lang="en">IP address</shortdesc>
<content type="string" default="${OCF_RESKEY_ipaddress_default}" />
</parameter>

<parameter name="cidr_netmask">
<longdesc lang="en">
The netmask for the interface in CIDR format. (ie, 24), or in
dotted quad notation  255.255.255.0).
</longdesc>
<shortdesc lang="en">Netmask</shortdesc>
<content type="string" default="${OCF_RESKEY_cidr_netmask_default}"/>
</parameter>

<parameter name="proto">
<longdesc lang="en">
Proto to match when finding network. E.g. "kernel".
</longdesc>
<shortdesc lang="en">Proto</shortdesc>
<content type="string" default="${OCF_RESKEY_proto_default}" />
</parameter>
</parameters>

<actions>
<action name="start" timeout="20s" />
<action name="stop" timeout="20s" />
<action name="monitor" depth="0" timeout="20s" interval="10s" />
<action name="validate-all" timeout="5s" />
<action name="meta-data" timeout="5s" />
</actions>
</resource-agent>
END
}

errorexit() {
	ocf_exit_reason "$*"
	exit $OCF_ERR_GENERIC
}

#
#	We can distinguish 3 cases: no preferred source address, a
#	preferred source address exists which matches that specified, and one
#	exists but doesn't match that specified.  srca_read() returns 1,0,2
#	respectively.
#
#	The output of route show is something along the lines of:
#
#		default via X.X.X.X dev eth1 src Y.Y.Y.Y
#
#	where the src clause "src Y.Y.Y.Y" may or may not be present

WS="[`echo -en ' \t'`]"
OCTET="[0-9]\{1,3\}"
IPADDR="\($OCTET\.\)\{3\}$OCTET"
SRCCLAUSE="src$WS$WS*\($IPADDR\)"
MATCHROUTE="\(.*${WS}\)\($SRCCLAUSE\)\($WS.*\|$\)"
FINDIF=$HA_BIN/findif

# findif needs that to be set
export OCF_RESKEY_ip=$OCF_RESKEY_ipaddress

srca_read() {
	# Capture the default route - doublequotes prevent word splitting...
	DEFROUTE="`$CMDSHOW`" || errorexit "command '$CMDSHOW' failed"

	# ... so we can make sure there is only 1 default route
	[ 1 -eq `echo "$DEFROUTE" | wc -l` ] || \
		errorexit "more than 1 default route exists"

	# But there might still be no default route
	[ -z "$DEFROUTE" ] && errorexit "no default route exists"

	# Sed out the source ip address if it exists
	SRCIP=`echo $DEFROUTE | sed -n "s/$MATCHROUTE/\3/p"`

	# and what remains after stripping out the source ip address clause
	ROUTE_WO_SRC=`echo $DEFROUTE | sed "s/$MATCHROUTE/\1\5/"`

	[ -z "$SRCIP" ] && return 1
	[ $SRCIP = $1 ] && return 0
	return 2
}

#
#	Add (or change if it already exists) the preferred source address
#	The exit code should conform to LSB exit codes.
#

srca_start() {
	srca_read $1

	rc=$?
	if [ $rc = 0 ]; then 
		rc=$OCF_SUCCESS
		ocf_log info "The ip route has been already set.($NETWORK, $INTERFACE, $ROUTE_WO_SRC)"
	else
		$IP2UTIL route replace $NETWORK dev $INTERFACE src $1 || \
			errorexit "command 'ip route replace $NETWORK dev $INTERFACE src $1' failed"

		$CMDCHANGE $ROUTE_WO_SRC src $1 || \
			errorexit "command '$CMDCHANGE $ROUTE_WO_SRC src $1' failed"
		rc=$?
	fi

	return $rc
}

#
#	Remove (if it exists) the preferred source address.
#	If one exists but it's not the same as the one specified, that's
#	an error.  Maybe that's the wrong behaviour because if this fails
#	then when IPaddr releases the associated interface (if there is one)
#	your default route will also get dropped ;-(
#	The exit code should conform to LSB exit codes.
#

srca_stop() {
	srca_read $1
	rc=$?

	if [ $rc = 1 ]; then
	# We do not have a preferred source address for now
	  ocf_log info "No preferred source address defined, nothing to stop"
	  exit $OCF_SUCCESS
	fi
	  
	[ $rc = 2 ] && errorexit "The address you specified to stop does not match the preferred source address"

	$IP2UTIL route replace $NETWORK dev $INTERFACE || \
		errorexit "command 'ip route replace $NETWORK dev $INTERFACE' failed"

	$CMDCHANGE $ROUTE_WO_SRC || \
		errorexit "command '$CMDCHANGE $ROUTE_WO_SRC' failed"

	return $?
}

srca_status() {
	srca_read $1

	case $? in
		0)	echo "OK"
			return $OCF_SUCCESS;;

		1)	echo "No preferred source address defined"
			return $OCF_NOT_RUNNING;;

		2)	echo "Preferred source address has incorrect value"
			return $OCF_ERR_GENERIC;;
	esac
}

# A not reliable IP address checking function, which only picks up those _obvious_ violations...
#
# It accepts IPv4 address in dotted quad notation, for example "192.168.1.1"
#
# 100% confidence whenever it reports "negative", 
# but may get false "positive" answer. 
# 
CheckIP() {
  ip="$1"
  case $ip in
    *[!0-9.]*) #got invalid char
	false;;
    .*|*.) #begin or end by ".", which is invalid
	false;;
    *..*) #consecutive ".", which is invalid
	false;;
    *.*.*.*.*) #four decimal dots, which is too many
	false;;
    *.*.*.*) #exactly three decimal dots, candidate, evaluate each field
	local IFS=.
	set -- $ip
	if
	    ( [ $1 -le 254 ] && [ $2 -le 254 ] && [ $3 -le 254 ] && [ $4 -le 254 ] )
	then
	    if [ $1 -eq 127 ]; then
		ocf_exit_reason "IP address [$ip] is a loopback address, thus can not be preferred source address"
		exit $OCF_ERR_CONFIGURED
	    fi
	else
	    true
	fi	   
	;;
    *) #less than three decimal dots
	false;;
  esac
  return $? # This return is unnecessary, this comment too :)
}

#
#       Find out which interface or alias serves the given IP address
#       The argument is an IP address, and its output
#       is an (aliased) interface name (e.g., "eth0" and "eth0:0").
#
find_interface_solaris() {


  $IFCONFIG $IFCONFIG_A_OPT | $AWK '{if ($0 ~ /.*: / && NR > 1) {print "\n"$0} else {print}}' |
  while read ifname linkstuff
  do
    : ifname = $ifname
    read inet addr junk
    : inet = $inet addr = $addr
    while
      read line && [ "X$line" != "X" ]
    do
      : Nothing
    done

    #  This doesn't look right for a box with multiple NICs.
    #  It looks like it always selects the first interface on
    #  a machine.  Yet, we appear to use the results for this case too...
    ifname=`echo "$ifname" | sed s'%:*$%%'`

    case $addr in
      addr:$BASEIP)	echo $ifname; return $OCF_SUCCESS;;
      $BASEIP)	echo $ifname; return $OCF_SUCCESS;;
    esac
  done
  return $OCF_ERR_GENERIC
}


#
#       Find out which interface or alias serves the given IP address
#       The argument is an IP address, and its output
#       is an (aliased) interface name (e.g., "eth0" and "eth0:0").
#
find_interface_generic() {

	local iface=`$IP2UTIL -o -f inet addr show | grep "\ $BASEIP" \
            | cut -d ' ' -f2 | grep -v '^ipsec[0-9][0-9]*$'`
        if [ -z "$iface" ]; then
            return $OCF_ERR_GENERIC
        else 
            echo $iface
            return $OCF_SUCCESS
        fi
}


#
#       Find out which interface or alias serves the given IP address
#       The argument is an IP address, and its output
#       is an (aliased) interface name (e.g., "eth0" and "eth0:0").
#
find_interface() {
    case "$SYSTYPE" in
	SunOS)
	 	IF=`find_interface_solaris $BASEIP`
        ;;
      *)
	 	IF=`find_interface_generic $BASEIP`
       ;;
       esac

  echo $IF
  return $OCF_SUCCESS;
}


ip_status() {

  BASEIP="$1"
  case "$SYSTYPE" in
    Darwin)
	# Treat Darwin the same as the other BSD variants (matched as *BSD)
	SYSTYPE="${SYSTYPE}BSD"
	;;
    *)
        ;;
  esac


  case "$SYSTYPE" in
      *BSD)
	  $IFCONFIG $IFCONFIG_A_OPT | grep "inet.*[: ]$BASEIP " >/dev/null 2>&1
	  if [ $? = 0 ]; then
	      return $OCF_SUCCESS
	  else
	      return $OCF_NOT_RUNNING
	  fi;;
      
      Linux|SunOS)		
	  IF=`find_interface "$BASEIP"`
	  if [ -z "$IF" ]; then
	      return $OCF_NOT_RUNNING
	  fi

	  case $IF in
	  	lo*)  
		    ocf_exit_reason "IP address [$BASEIP] is served by loopback, thus can not be preferred source address"
		    exit $OCF_ERR_CONFIGURED
		    ;;
		*)return $OCF_SUCCESS;;
	  esac
	  ;;
	  
      *)		
	  if [ -z "$IF" ]; then
	      return $OCF_NOT_RUNNING
	  else
	      return $OCF_SUCCESS
	  fi;;
  esac
}


srca_validate_all() {

	if [ -z "$OCF_RESKEY_ipaddress" ]; then
		#  usage
		ocf_exit_reason "Please set OCF_RESKEY_ipaddress to the preferred source IP address!"
		return $OCF_ERR_CONFIGURED
	fi


	if ! [ "x$SYSTYPE" = "xLinux" ]; then
		# checks after this point are only relevant for linux.
		return $OCF_SUCCESS
	fi

	check_binary $AWK
	case "$SYSTYPE" in
		*BSD|SunOS)
			check_binary $IFCONFIG
			;;
	esac

#	The IP address should be in good shape
	if CheckIP "$ipaddress"; then
	  : 
	else
	  ocf_exit_reason "Invalid IP address [$ipaddress]"
	  return $OCF_ERR_CONFIGURED
	fi

	if ocf_is_probe; then
	  return $OCF_SUCCESS
	fi

#	We should serve this IP address of course
	if ip_status "$ipaddress"; then
	  :
	else
	  ocf_exit_reason "We are not serving [$ipaddress], hence can not make it a preferred source address"
	  return $OCF_ERR_INSTALLED
	fi
	return $OCF_SUCCESS
}

if
  ( [ $# -ne 1 ] )
then
  usage
  exit $OCF_ERR_ARGS
fi

# These operations do not require the OCF instance parameters to be set
case $1 in
	meta-data)	meta_data 
			exit $OCF_SUCCESS
			;;
	usage)		usage
			exit $OCF_SUCCESS
			;;
	*)	
			;;
esac

ipaddress="$OCF_RESKEY_ipaddress"

srca_validate_all
rc=$?
if [ $rc -ne $OCF_SUCCESS ]; then
	case $1 in
		# if we can't validate the configuration during a stop, that
		# means the resources isn't configured correctly. There's no way
		# to actually stop the resource in this situation because there's
		# no way it could have even started. Return success here
		# to indicate that the resource is not running, otherwise the
		# stop action will fail causing the node to be fenced just because
		# of a mis configuration.
		stop) exit $OCF_SUCCESS;;
		*)    exit $rc;;
	esac
fi

findif_out=`$FINDIF -C`
rc=$?
[ $rc -ne 0 ] && {
    ocf_exit_reason "[$FINDIF -C] failed"
    exit $rc
}

INTERFACE=`echo $findif_out | awk '{print $1}'`
NETWORK=`$IP2UTIL route list dev $INTERFACE scope link $PROTO match $ipaddress|grep -m 1 -o '^[^ ]*'`

case $1 in
	start)		srca_start $ipaddress
			;;
	stop)		srca_stop $ipaddress
			;;
	status)		srca_status $ipaddress
			;;
	monitor)	srca_status $ipaddress
			;;
	validate-all)	srca_validate_all
			;;
	*)		usage
			exit $OCF_ERR_UNIMPLEMENTED
			;;
esac

exit $?

#
# Version 0.3  2002/11/04 17:00:00 John Sutton <john@scl.co.uk>
# Name changed from IPsrcroute to IPsrcaddr and now reports errors
# using ha_log rather than on stderr.
#
# Version 0.2  2002/11/02 17:00:00 John Sutton <john@scl.co.uk>
# Changed status output to "OK" to satisfy ResourceManager's
# we_own_resource() function.
#
# Version 0.1  2002/11/01 17:00:00 John Sutton <john@scl.co.uk>
# First effort but does the job?
#
